package com.example.lock.redis.impl;

import com.example.lock.entity.RedisLockInfo;
import com.example.lock.redis.RedisLock;
import com.example.util.ThreadPoolUtil;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * @Description: redis 实现分布式锁 针对获取锁的线程进行续命处理
 * @Author: mingtian
 * @CreateDate: 2020/12/14 14:14
 * @Version: 1.0
 */
@Component
public class LocalRedisLockImpl implements RedisLock {

    /**
     * 打印日志
     */
    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 存放获取锁对象
     */
    private static Map<Thread, RedisLockInfo> cacheThreadLock = new ConcurrentHashMap<>();


    /**
     * redis 锁 key
     */
    private String redisLockKey = "redisLockKey";

    /**
     * 锁超时时间
     */
    private Long lockTimeOut = 30L;

    /**
     * 重试获取超时时间
     */
    private Long retryTimeOut = 30000L;

    /**
     * 续命次数上限
     */
    private Integer MAX_LIFE_COUNT = 3;

    /**
     * 续命间隔时间 续命间隔时间应该小于 redis key 失效时间 否则续命将没有任何意义
     */
    private int interval = 10000;


    /**
     * 获取锁
     *
     * @return
     */
    @Override
    public boolean getLock() {

        // 从缓存中获取锁对象
        Thread currentThread = Thread.currentThread();
        RedisLockInfo redisLockInfo = cacheThreadLock.get(currentThread);
        if (!Objects.isNull(redisLockInfo) && redisLockInfo.isState()) {
            logger.info(">>>在缓存中获取到锁,可以直接复用!<<<");
            return true;
        }
        // 生成锁Id
        String lockId = UUID.randomUUID().toString();
        try {
            // 开始执行时间
            long startTime = System.currentTimeMillis();
            for (; ; ) {
                // 1.获取锁 设置超时时间 避免 死锁现象
                Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(redisLockKey, lockId, lockTimeOut, TimeUnit.MINUTES);
                if (lock) {
                    logger.info(">>>获取锁成功!<<<");
                    RedisLockInfo newLockInfo = new RedisLockInfo(lockId, currentThread, lockTimeOut);
                    cacheThreadLock.put(currentThread, newLockInfo);
                    // 开始监听续命 针对于获取锁线程 局部的 建议使用线程池
//                    new Thread(new LifeExtensionThread(newLockInfo, interval)).start();
                    ThreadPoolUtil.getThreadPoolExecutorCPU().execute(new LifeExtensionThread(newLockInfo, interval));
                    return true;
                }
                // 执行结束时间
                long endTime = System.currentTimeMillis();
                // 判断重试超时时间
                if (endTime - startTime > retryTimeOut) {
                    logger.info(">>>重试时间已经过了,不再继续重试!<<<");
                    return false;
                }
                // 休眠 500 毫秒之后 在重试
                Thread.sleep(50);
                return false;
            }
        } catch (Exception e) {
            logger.error(">>>errorInfo:{}<<<", e);
            return false;
        }
    }

    /**
     * 释放锁
     */
    @Override
    public boolean releaseLock() {
        // 当前线程
        Thread currentThread = Thread.currentThread();
        // 根据线程查询当前 redis 锁信息
        RedisLockInfo redisLockInfo = cacheThreadLock.get(currentThread);
        if (Objects.isNull(redisLockInfo)) {
            return false;
        }
        // 查询 redis 存储 锁 Id
        String redisLockId = stringRedisTemplate.opsForValue().get(redisLockKey);
        if (StringUtils.isEmpty(redisLockId)) {
            logger.info(">>>key 不存在,redisLockKey:{}<<<", redisLockKey);
            return false;
        }
        // 锁对象的中 锁Id
        String lockId = redisLockInfo.getLockId();
        if (StringUtils.isEmpty(lockId)) {
            logger.info(">>>该锁对象中不存在锁Id,curThread:{}<<<", currentThread);
            return false;
        }
        // 对比是否是自己线程的锁对象
        if (!redisLockId.equals(lockId)) {
            logger.info(">>>不是自己线程调用删除 key 操作<<<");
        }
        return stringRedisTemplate.delete(redisLockKey);
    }


    /**
     * 续命线程(针对于单个获取到锁线程做处理)
     * 当我们的获取锁的jvm业务执行时间>过期key的超时时间 应该实现续命：
     * 当key过期的时候：走事件回调到客户端。---时间在延长
     * 延长过期key 应该是提前的。通过定时任务提前延长过期key
     * 算法实现：
     * 开启一个定时任务，每隔一段时间检测获取到锁的线程，延长该过期key的时间
     */
    private class LifeExtensionThread implements Runnable {
        /**
         * redis 锁对象
         */
        private RedisLockInfo redisLockInfo;
        /**
         * 线程休眠时间(续命间隔时间)
         */
        private int interval;

        /**
         * 传入参数
         *
         * @param redisLockInfo
         * @param interval
         */
        public LifeExtensionThread(RedisLockInfo redisLockInfo, int interval) {
            this.redisLockInfo = redisLockInfo;
            this.interval = interval;
        }

        @Override
        public void run() {
            logger.info("开始进行续命处理...");
            while (true) {
                try {
                    // 续命间隔时间
                    Thread.sleep(interval);
                    // 续命检测
                    if (Objects.isNull(redisLockInfo)) {
                        return;
                    }
                    // 获取拿到锁的线程
                    Thread lockThread = redisLockInfo.getLockThread();
                    if (redisLockInfo.isState() && lockThread.isInterrupted()) {
                        logger.info(">>>当前获取到锁的线程已经执行完毕,手动释放锁.<<<");
                        return;
                    }
                    // 判断续命的次数 如果续命次数大于3 则释放锁 不在进行续命处理
                    Integer lifeCount = redisLockInfo.getLifeCount();
                    logger.info("第{}次续命", lifeCount);
                    if (lifeCount > MAX_LIFE_COUNT) {
                        logger.info(">>>续命次数已经达到续命上限,不在进行续命,手动释放锁.<<<");
                        // 1.回滚事务
                        // 2.释放锁
                        releaseLock();
                        // 3.停止该线程
                        lockThread.interrupt();
                        return;
                    }
                    // 进行续命处理
                    stringRedisTemplate.expire(redisLockKey, redisLockInfo.getExpire(), TimeUnit.SECONDS);
                } catch (Exception e) {
                    logger.error(">>>errorInfo:{}<<<", e);
                }

            }
        }
    }
}